/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
/*
 * TrainingBook
 * Copyright (C) Arnaud Duval 2010 <arnaud.duval@gmail.com>
 * 
 * TrainingBook is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * TrainingBook is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along
 * with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <osm-map-area.hpp>
#include <config.hpp>

#include <iostream>
#include <cmath>


#define OSM_TILE_WIDTH 256
#define OSM_TILE_HEIGHT 256
#define OSM_MAX_ZOOM_LEVEL 18
#define OSM_MIN_ZOOM_LEVEL 0


///@todo The zoom level should be automatically set with given bounding box

///@brief Default constructor
OsmMapArea::OsmMapArea()
{
    add_events(Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON1_MOTION_MASK | Gdk::BUTTON_RELEASE_MASK);
    //tCache = OsmMapTileCache(std::string(TRAININGBOOK_USER_DIR)+TRAININGBOOK_FILE_SEPARATOR+"cache");
    //tCache = OsmMapTileCache("/home/arnaud/cache");
    zoomLevel = 3;
    statusBar = NULL;
    isDrawing = false;
}

///@brief Constructor specifying the location of tiles repository
///@param osmTilesRepository URL of the tiles repository
///@param osmTilesCacheDir Path to the tiles caches directory
OsmMapArea::OsmMapArea(std::string osmTilesRepository, std::string osmTilesCacheDir)
{
    add_events(Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON1_MOTION_MASK | Gdk::BUTTON_RELEASE_MASK);
    //tCache = OsmMapTileCache(std::string(TRAININGBOOK_USER_DIR)+TRAININGBOOK_FILE_SEPARATOR+"cache", osmTilesRepository);
    //tCache = OsmMapTileCache("/home/arnaud/cache", osmTilesRepository);
    tCache = osmTilesCacheDir;
    zoomLevel = 3;
    statusBar = NULL;
    isDrawing = false;
}

///@brief Destructor
OsmMapArea::~OsmMapArea()
{

}

///@brief Set the bounding box
///@param DminLat Minimum value of latitude IN DEGREES
///@param DmaxLat Maximum value of latitude IN DEGREES
///@param DminLon Minimum value of longitude IN DEGREES
///@param DmaxLon Maximum value of longitude IN DEGREES
void OsmMapArea::SetBoundingBox(double DminLat, double DmaxLat, double DminLon, double DmaxLon)
{
	bbox = OsmMapBoundingBox(DminLat, DmaxLat, DminLon, DmaxLon);
}


///@brief Add a track to plot
///@param t GPS track to add
void OsmMapArea::AddTrack(Track t)
{
	tracks.push_back(t);
	std::list<double> lats = t.GetLatList();
	std::list<double> lons = t.GetLonList();
	DegreesToMercator(lons, lats);
	plotsX.push_back(lons);
	plotsY.push_back(lats);
}

///@brief Override default signal handler
bool OsmMapArea::on_expose_event(GdkEventExpose* event)
{
    Glib::RefPtr<Gdk::Window> window = get_window();
    if(window)
    {
        RefreshMap(window);
        RefreshTracks(window);
    }
    return true;
}

///@brief Increase the zoom level
///@param redraw Specify if the map needs to be redrawn
void OsmMapArea::IncreaseZoomLevel(bool redraw)
{
	if(zoomLevel != OSM_MAX_ZOOM_LEVEL)
	{
		zoomLevel++;
		if(redraw)
			queue_draw();
		std::cout << "Current zoom level : " << zoomLevel << "\n";
	}
}

///@brief Decrease the zoom level
///@param redraw Specify if the map needs to be redrawn
void OsmMapArea::DecreaseZoomLevel(bool redraw)
{
	if(zoomLevel != OSM_MIN_ZOOM_LEVEL)
	{
		zoomLevel--;
		if(redraw)
			queue_draw();
		std::cout << "Current zoom level : " << zoomLevel << "\n";
	}
}

///@brief Set the zoom level
///@param zoom The new zoom level
///@param redraw Specify if the map needs to be redrawn
void OsmMapArea::SetZoomLevel(unsigned int zoom, bool redraw)
{
	if(zoom < OSM_MIN_ZOOM_LEVEL)
	{
		zoomLevel = OSM_MIN_ZOOM_LEVEL;
	}
	else if(zoom > OSM_MAX_ZOOM_LEVEL)
	{
		zoomLevel = OSM_MAX_ZOOM_LEVEL;
	}
	else
		zoomLevel = zoom;
	if(redraw)
			queue_draw();
		std::cout << "Current zoom level : " << zoomLevel << "\n";
}

///@brief Get the zoom level
unsigned int OsmMapArea::GetZoomLevel()
{
	return zoomLevel;
}

///@brief Refresh the map
///@param window Pointer to the current window
void OsmMapArea::RefreshMap(Glib::RefPtr<Gdk::Window> window)
{
	window->set_cursor(Gdk::Cursor(Gdk::WATCH));
	
	Gtk::Allocation allocation = get_allocation();
	const int allo_width = allocation.get_width();
	const int allo_height = allocation.get_height();
	
	//Coordonnées au centre de la fenêtre
	double cLat = bbox.GetCenterLat();
	double cLon = bbox.GetCenterLon();

	//Calcul de x et y en Mercator
	double x = cLon;
	double y = log(tan(cLat) + 1./cos(cLat));
	
	//Nombre de tuiles dans la map
	unsigned long int n = pow(2, zoomLevel);
	//Numéros de tuile
	long int xtile = (long int)(((1.+(x/M_PI))/2.)*n);
	long int ytile = (long int)(((1.-(y/M_PI))/2.)*n);
	
	//Nombre de tuiles à la verticale et à l'horizontale
	int nTileHor = allo_width / 256 + 3;
	int nTileVer = allo_height / 256 + 4;
	
	//Positionnement de la tuile du centre
	//Coordonnées du coin haut gauche
	double xTL = (((2.*xtile) / n) - 1.)*M_PI;
	double yTL = -(((2.*ytile) / n) - 1.)*M_PI;
	//Coordonnée du coin bas droite
	double xBR = (((2.*(xtile+1)) / n) - 1.)*M_PI;
	double yBR = -(((2.*(ytile+1)) / n) - 1.)*M_PI;

	//Coordonnées X et Y (dans le context)
	int Xtl = (int)(256.*n/(2.*M_PI) * (xTL - x));
	int Ytl = (int)(-256.*n/(2.*M_PI) * (yTL - y));

	//Création des context nécessaires
	Cairo::RefPtr<Cairo::Context> *cr = new Cairo::RefPtr<Cairo::Context>[nTileHor*nTileVer];
	Cairo::RefPtr<Cairo::ImageSurface> *image_surface_ptr = new Cairo::RefPtr<Cairo::ImageSurface>[nTileHor*nTileVer];

	for(unsigned int i = 0 ; i < nTileHor*nTileVer ; i++)
	{
		cr[i] = window->create_cairo_context();
    }

	for(long int itile = xtile - nTileHor / 2 ; itile < xtile + nTileHor / 2 ; itile++)
	{
		for(long int jtile = ytile - nTileVer / 2 ; jtile < ytile + nTileVer / 2 ; jtile++)
		{
			unsigned int icontext = (itile - (xtile - nTileHor / 2))*nTileVer + (jtile - (ytile - nTileVer / 2));
			try
			{
                image_surface_ptr[icontext] = Cairo::ImageSurface::create_from_png(tCache.GetTile(itile, jtile, zoomLevel).c_str());
			}
			catch(std::exception& e)
			{
                std::cerr << "Exception  : " << e.what() << std::endl;
			}

			cr[icontext]->save();
			//Coordonnées du coin haut gauche de la tuile
			double xcoord = allo_width/2 + (itile - xtile)*OSM_TILE_WIDTH + Xtl;
			double ycoord = allo_height/2 + (jtile - ytile)*OSM_TILE_HEIGHT + Ytl;

			cr[icontext]->set_source(image_surface_ptr[icontext], xcoord, ycoord);

			//Coordinates for the rectangle
			double xcoordRect = xcoord;
			double ycoordRect = ycoord;
			if(xcoordRect < 0.)
				xcoordRect = 0.;
			if(ycoordRect < 0.)
				ycoordRect = 0.;
			cr[icontext]->rectangle(xcoordRect, ycoordRect, xcoord + OSM_TILE_WIDTH, ycoord + OSM_TILE_HEIGHT);
			cr[icontext]->clip();
			cr[icontext]->paint();
			cr[icontext]->restore();
		}
	}
	delete[] cr;
	delete[] image_surface_ptr;
	window->set_cursor(Gdk::Cursor(Gdk::LEFT_PTR));
}

///@brief Refresh the tracks
///@param window Pointer to the current window
void OsmMapArea::RefreshTracks(Glib::RefPtr<Gdk::Window> window)
{
	std::list<std::list<double> >::iterator litX(plotsX.begin()), lendX(plotsX.end());
	std::list<std::list<double> >::iterator litY(plotsY.begin());
	
	for(; litX != lendX ; ++litX)
	{
		PlotTrack(window, (*litX), (*litY));
		++litY;
	}
	if(isDrawing)
      PlotTrack(window, drawX, drawY);
}

///@brief Plot a track on the map
///@param window Pointer to the current window
///@param valX X values in te Mercator projection
///@param valY Y values in the Mercator projection
void OsmMapArea::PlotTrack(Glib::RefPtr<Gdk::Window> window, std::list<double> valX, std::list<double> valY)
{
	Gtk::Allocation allocation = get_allocation();
	const int allo_width = allocation.get_width();
	const int allo_height = allocation.get_height();
	//Coordonnées au centre de la fenêtre
	double cLat = bbox.GetCenterLat();
	double cLon = bbox.GetCenterLon();

	//Calcul de x et y en Mercator
	double x = cLon;
	double y = log(tan(cLat) + 1./cos(cLat));
	//Nombre de tuiles dans la map
	unsigned long int n = pow(2, zoomLevel);
	//Scaling coefficient
	double coef = 256.*n/(2.*M_PI);
	Cairo::RefPtr<Cairo::Context> cr = window->create_cairo_context();
	cr->set_line_width(2.);
	cr->set_source_rgb(1.,0.,0.);
	cr->rectangle(0., 0., allo_width, allo_height);
	cr->clip();
	std::list<double>::iterator litx(valX.begin()), lendx(valX.end());
	std::list<double>::iterator lity(valY.begin());
	if(valX.size() != valY.size())
	{
		std::cerr << "Error in data to plot : not the same number of values for Longitude and Latitude\n";
		return;
	}
	for(;litx != lendx ; ++litx)
	{
		if(litx == valX.begin())
			cr->move_to(allo_width/2 + coef * ((*litx)-x), allo_height/2 - coef * ((*lity) - y));
		else
			cr->line_to(allo_width/2 +coef * ((*litx)-x), allo_height/2 - coef * ((*lity) - y));
		++lity;
	}
	cr->stroke();
}

///@brief Convert longitude and latitude values list in Mercator coordinates
///@param lons Longitude values list in degrees, will be converted in X
///@param lats Latitude values list in degrees, will ne converted in Y
void OsmMapArea::DegreesToMercator(std::list<double>& lons, std::list<double>& lats)
{
	std::list<double>::iterator litLon(lons.begin()), lendLon(lons.end());
	for(; litLon != lendLon ; ++litLon)
	{
		double val = (*litLon);
		val*=(M_PI/180.);
		(*litLon) = val;
	}

	std::list<double>::iterator litLat(lats.begin()), lendLat(lats.end());
	for(; litLat != lendLat ; ++litLat)
	{
		double val = (*litLat);
		val*=(M_PI/180.);
		(*litLat) = log(tan(val) + 1./cos(val));
	}
}

///@brief Convert Mercator coordinates into latitude and longitude
///@param x x value, will be converted in longitude
///@param y y value, will be converted in latitude
void OsmMapArea::MercatorToDegrees(double& x, double& y)
{
    x *= 180./M_PI;
    double temp = 2.*atan(exp(y)) - 0.5*M_PI;
    y = temp*180./M_PI;
}

///@brief Translate the map
///@param dx x displacement IN PIXELS
///@param dy y displacement IN PIXELS
///@param redraw true if the map need to be redrawn
void OsmMapArea::Move(int dx, int dy, bool redraw)
{
	//conversion des pixels en coordonnées Mercator
	unsigned long int n = pow(2, zoomLevel);
	double coef = 256.*n/(2.*M_PI);
	double Dx = dx / coef;
	double Dy = dy / (-coef);
	Dy = atan(sinh(Dy));	//Transformation Mercator vers radian

	bbox.Translate(Dx, Dy);

	if(redraw)
		queue_draw();
}

///@brief Override default signal hander for button press
bool OsmMapArea::on_button_press_event(GdkEventButton* event)
{
	if((event->type == GDK_2BUTTON_PRESS)&&(event->button == 1))
	{
		Gtk::Allocation allocation = get_allocation();
		const int allo_width = allocation.get_width();
		const int allo_height = allocation.get_height();
		Move(-allo_width/2 + (int)event->x, -allo_height/2 + (int)event->y, false);
		IncreaseZoomLevel();
		return true;
	}
	else if((event->type == GDK_2BUTTON_PRESS)&&(event->button == 3))
	{
		Gtk::Allocation allocation = get_allocation();
		const int allo_width = allocation.get_width();
		const int allo_height = allocation.get_height();
		Move(-allo_width/2 + (int)event->x, -allo_height/2 + (int)event->y, false);
		DecreaseZoomLevel();
		return true;
	}
	else if((event->type == GDK_BUTTON_PRESS)&&(event->button == 1))
	{
		mouse_x_on_press = (int)event->x;
		mouse_y_on_press = (int)event->y;
		return true;
	}
	else if((event->type == GDK_BUTTON_PRESS)&&(event->button == 2))
    {
        Gtk::Allocation allocation = get_allocation();
        const int allo_width = allocation.get_width();
        const int allo_height = allocation.get_height();
        //coordinates of area center
        double cLat = bbox.GetCenterLat();
        double cLon = bbox.GetCenterLon();
        //Mercator coordinates of area center
        double cx = cLon;
        double cy = log(tan(cLat) + 1./cos(cLat));
        //Some useful coefficients
        unsigned long int n = pow(2, zoomLevel);
        double coef = 256.*n/(2.*M_PI);

        double lon = event->x;
        double lat = event->y;

        lon = (lon - allo_width/2)/coef +cx;
        lat = (lat - allo_height/2)/(-coef) + cy;

        drawX.push_back(lon);
        drawY.push_back(lat);
        
        MercatorToDegrees(lon, lat);
        std::cout << "lon : " << lon << "  lat : " << lat << "\n";

        if(!isDrawing)
        {
            isDrawing = true;
            TrackSegment newSeg;
            //Ajouter le point en cours
            DateTime date;
            TrackPoint newPoint(lat, lon, 0., 0., date);

            newSeg.AddPoint(newPoint);
            drawnTrack.AddSegment(newSeg);
        }
        else
        {
            TrackSegment seg = *(drawnTrack.GetFirstSegment());
            DateTime date;
            TrackPoint newPoint(lat, lon, 0., 0., date);
            seg.AddPoint(newPoint);
        }
        queue_draw();
        return true;

    }
	else
		return false;
}

bool OsmMapArea::on_button_release_event(GdkEventButton* event)
{
	if((event->type == GDK_BUTTON_RELEASE)&&(event->button == 1))
	{
		return true;
	}
	else
		return false;
}

///@brief Override default signal handler for mouse motion
bool OsmMapArea::on_motion_notify_event(GdkEventMotion* event)
{

	Move(mouse_x_on_press - (int)event->x, mouse_y_on_press - (int)event->y);
	mouse_x_on_press = (int)event->x;
	mouse_y_on_press = (int)event->y;
	return true;

	return false;
}

///@brief Clear the tracks
void OsmMapArea::ClearTracks()
{
	tracks.clear();
	plotsX.clear();
	plotsY.clear();
	queue_draw();
}

///@brief Set the tiles repository
///@param osmTilesRepository URL of the OSM tiles repository
void OsmMapArea::SetTilesRepository(std::string osmTilesRepository)
{
    tCache.SetTilesRepository(osmTilesRepository);   
}

///@brief Set the tiles cache
///@param osmTilesCache Path to the OSM tiles cache
void OsmMapArea::SetTilesCache(std::string osmTilesCache)
{
    tCache.SetTilesCache(osmTilesCache);
}

///@brief Set the status bar on wich informations about the map area should be displayed
///@param pBar Pointer to the status bar
void OsmMapArea::SetStatusBar(Gtk::Statusbar* pBar)
{
    statusBar = pBar;
    tCache.SetStatusBar(statusBar);
}